Skip to content

MySQL 三题整理:索引、MVCC 与事务失效

这篇笔记沉淀当前这一轮练到的 3 个题点:

  • MySQL 索引为什么快,什么是回表、覆盖索引、最左前缀
  • 事务隔离级别和 MVCC 是怎么配合工作的
  • Spring 事务为什么会失效

第三题严格说更偏 Spring 与 AOP,但和数据库事务放在一起复习时非常高频,所以先作为事务专题延伸一起保留。

关联项目:

  • [[黑马点评]]

1. MySQL 索引为什么快,什么是回表、覆盖索引、最左前缀

标准答案

MySQL 索引快,核心原因是底层一般采用 B+ 树结构。B+ 树的树高比较低,在大数据量下也通常只需要少量层级就能定位到目标数据,所以能明显减少磁盘 IO;同时叶子节点是有序连接的,对范围查询和排序也比较友好。

回表是指通过二级索引查到主键值后,还要再去主键索引里查完整行数据。因为 InnoDB 的二级索引叶子节点里存放的不是完整行,而是主键值。

覆盖索引是指本次查询需要的字段,索引本身已经全部包含了,这样就可以直接从索引里返回结果,不需要再回表,所以通常更快。

最左前缀是联合索引的使用规则。比如联合索引是 (a, b, c),查询时会尽量从最左边开始连续匹配。也就是说先用 a,在 a 确定后再看 b,在 ab 都确定后再看 c

高频追问

  • 二级索引为什么会回表?
  • 覆盖索引为什么更快?
  • 一个查询明明走了索引,为什么还是可能慢?
  • 联合索引在 B+ 树里到底是怎么排的?
  • (a, b, c) 的最左前缀到底是什么意思?
  • where a=1 and c=1where b=1 and c=1where a=1 and b>2 and c=3 分别怎么用索引?

关键补缺

二级索引是什么

二级索引就是非主键索引。
在 InnoDB 里:

  • 主键索引(聚簇索引)叶子节点存的是整行数据
  • 二级索引(辅助索引)叶子节点存的是主键值和索引列的值

所以通过二级索引查数据时,如果查询字段不在这个索引里,就必须先拿到主键,再去主键索引查整行,这就是回表。

为什么覆盖索引更快

覆盖索引快,不是因为“也用了索引”,而是因为它避免了回表。

如果一个查询走的是普通二级索引,但查出的字段不全在索引里,那么数据库还得再去主键索引做一次 B+ 树查找。命中的数据一多,这个过程会产生大量随机 IO,所以整体仍然可能很慢。

你可以记成一句话:

覆盖索引快,是因为查询字段都在索引里,省掉了回表这一步。

联合索引在 B+ 树里的组织方式

如果联合索引是 (a, b, c),那这棵 B+ 树里的键不是分别按 abc 排,而是按 (a, b, c) 这个复合键整体排序。

排序规则是:

  1. 先按 a
  2. a 相同再按 b
  3. ab 都相同再按 c

所以:

  • a 在全局上有序
  • b 只有在 a 确定后才局部有序
  • c 只有在 ab 都确定后才局部有序

这就是最左前缀原则的底层原因。

三种典型查询怎么理解

  1. where a = 1 and c = 1

    可以先利用 a=1 找到一段范围,但因为中间缺了 bc 不能继续像等值匹配那样充分利用索引排序能力,所以通常理解为:

    • 先用 a
    • 再对 c 额外过滤
  2. where b = 1 and c = 1

    一般用不好这个联合索引,因为最左边的 a 没有出现。

  3. where a = 1 and b > 2 and c = 3

    这时:

    • a 能用
    • b 也能用来定位范围起点
    • 但一旦到了 b > 2 这种范围条件,后面的 c 通常就不能继续完整利用索引做精确定位了

本质原因是:
c 只有在 a 和 b 都固定时才有序;而 b>2 已经把 b 变成了一个范围,后面的 c 就只能在扫描过程中再过滤。`

易错点

  • 不要把“索引快”只答成“因为排序了”
  • 不要把回表说成“查询字段没建索引就叫回表”
  • 不要把联合索引理解成“where 条件个数小于等于索引列数就能用”

2. 事务隔离级别和 MVCC 是怎么配合工作的

标准答案

MySQL 常见的四种隔离级别是:

  • 读未提交
  • 读已提交
  • 可重复读
  • 可串行化

从能力上看:

  • 读未提交:脏读、不可重复读、幻读都可能发生
  • 读已提交:可以避免脏读,但不可重复读和幻读仍可能发生
  • 可重复读:可以避免脏读和不可重复读,在 MySQL InnoDB 里对幻读也做了比较强的控制
  • 可串行化:并发能力最低,但隔离最强

MVCC 主要工作在 读已提交可重复读 这两个级别中,核心是用不同的 Read View 管理快照读的可见性。

RC 下,每次快照读都会生成新的 Read View,所以两次读取之间,如果别的事务提交了修改,你下一次再读就可能看到新值,这样虽然避免了脏读,但仍然会发生不可重复读。

RR 下,事务第一次快照读生成的 Read View 会被后续快照读复用,所以能保证快照读场景下的可重复读。

但要注意,MVCC 主要解决的是快照读下的可见性问题;当前读场景下要防止别的事务插入新记录导致幻读,主要还是依赖间隙锁或临键锁,而不是把幻读简单全算成 MVCC 单独解决的。

高频追问

  • RC 和 RR 在 Read View 上最大的区别是什么?
  • MVCC 为什么能防脏读?
  • RR 为什么能防不可重复读?
  • 幻读为什么不能简单都说是 MVCC 单独解决的?
  • 间隙锁和排他锁是 MVCC 的机制吗?
  • 快照读和当前读分别是什么?

关键补缺

MVCC 和锁机制要拆开

这是这题最容易混的点。

  1. MVCC

MVCC 解决的是:

  • 不加锁普通读时,该看哪个版本
  • 不同事务在同一时刻看到哪个事务版本的数据

它的核心依赖:

  • undo log
  • 隐藏字段
  • Read View

所以你可以把它概括成:

MVCC 管可见性。

  1. 锁机制

锁机制解决的是:

  • 别人能不能改
  • 别人能不能插
  • 当前读时并发怎么控制

包括:

  • 排他锁
  • 间隙锁
  • 临键锁

所以你可以把它概括成:

锁机制管并发修改。

为什么 RC 能防脏读,但防不了不可重复读

因为 RC 每次读都会生成新的 Read View
未提交的数据不会进入你的可见范围,所以能防脏读;但如果别的事务在两次读之间提交了修改,你下一次生成新的 Read View 时就会看到新值,因此不可重复读仍然可能发生。

为什么 RR 能防快照读下的不可重复读

因为 RR 在事务第一次快照读时生成 Read View,后续快照读复用这张视图。
这样你多次读到的是同一事务视角下的数据,所以快照读场景能保持可重复读。

为什么幻读不能简单都算 MVCC 解决

如果只是普通快照读,在 RR 下很多时候你看起来“没有幻读”,本质是因为你一直在看第一次的快照。

但如果是:

  • select ... for update
  • update
  • delete

这种当前读,你如果想锁住一个范围,不让别人插入新记录,靠的就不是 MVCC,而是 间隙锁 / 临键锁

间隙锁到底负责什么

更稳的说法是:

间隙锁主要是为了防止别的事务在某个范围内插入新记录,从而避免当前读场景下的幻读。

不要笼统说成“防止增加或删除”,重点是:

防插入。

易错点

  • 不要把 MVCC 和间隙锁混成一个东西
  • 不要把“RR 防幻读”简单粗暴全归功于 MVCC
  • 不要把快照读和当前读混着答

3. Spring 事务为什么会失效

标准答案

Spring 事务失效最典型的原因是事务基于 AOP 代理实现,如果调用没有经过代理对象,事务就不会生效。最常见的是同类内部 this 自调用绕过代理。

除此之外,还有几个很高频的失效场景。第一,@Transactional 标在非 public 方法上通常不生效,因为代理默认只拦截 public 方法。第二,异常被 try-catch 吞掉了,代理感知不到异常,就不会回滚。第三,抛的是受检异常,比如 IOException,而没有配置 rollbackFor,Spring 默认也不会回滚。第四,当前对象根本没有交给 Spring 容器管理,比如自己 new 出来的对象,没有代理,自然也没有事务。第五,底层数据库如果用的是不支持事务的引擎,比如 MyISAM,那 Spring 配了事务也没用。再补一个,传播行为如果配置成 NOT_SUPPORTED 这类非事务模式,当前方法也可能看起来像事务失效。

高频追问

  • 为什么 this 自调用会失效?
  • 除了自调用,Spring 事务还有哪些高频失效场景?
  • 为什么异常被吞掉后事务不回滚?
  • 为什么 checked exception 默认不回滚?
  • 为什么数据库引擎也会影响 Spring 事务是否生效?
  • 哪些传播行为会让当前方法不跑在事务里?

关键补缺

Spring 事务本质依赖代理

Spring 声明式事务不是魔法,它本质上是:

  • Spring 为 Bean 生成代理对象
  • 调用代理对象的方法时,先开启事务
  • 方法成功执行后提交
  • 如果代理感知到满足规则的异常,就回滚

所以只要“不经过代理”,事务就容易失效。

常见失效场景

  1. this 自调用

同一个类内部通过 this.xxx() 调用事务方法,不经过代理对象。

  1. 非 public 方法

默认事务代理主要拦截 public 方法,非 public 方法通常不生效。

  1. 异常被吞

如果你 try-catch 之后不继续抛,代理会认为方法执行成功,于是提交事务。

  1. 受检异常默认不回滚

Spring 默认只对:

  • RuntimeException
  • Error

回滚。
IOException 这类 checked exception 默认不会回滚,所以很多项目里会显式写:

@Transactional(rollbackFor = Exception.class)

  1. 对象不是 Spring 管理的 Bean

如果对象是自己 new 的,就没有代理对象,也就没有事务切面。

  1. 数据库本身不支持事务

比如 MySQL 的 MyISAM 引擎,不支持事务,Spring 再怎么配都没法真的回滚。

  1. 传播行为配置不当

例如:

  • NOT_SUPPORTED
  • NEVER

这类传播行为会让当前方法以非事务方式执行,效果就像事务没生效。

易错点

  • 不要只会答 this 自调用
  • 不要说“只要报错就一定回滚”
  • 不要忽略底层数据库引擎

三题主线关系

这三题虽然分属不同层,但非常适合连起来复习:

  1. 索引 解决数据库查询为什么能快、什么情况下会慢。

  2. 事务隔离级别与 MVCC 解决数据库并发读写时,数据可见性和隔离性的本质问题。

  3. Spring 事务失效 解决业务代码明明写了事务,为什么到工程落地层还是可能失效。

一句话串起来:

索引考的是查询性能,MVCC 考的是并发可见性,Spring 事务失效考的是工程层事务能不能真正生效。